#!/usr/bin/python
# Copyright (c) 2018, Milan Ilic <milani@nordeus.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

# Make coding more python3-ish
from __future__ import annotations

DOCUMENTATION = r"""
module: one_image
short_description: Manages OpenNebula images
description:
  - Manages OpenNebula images.
requirements:
  - pyone
extends_documentation_fragment:
  - community.general.opennebula
  - community.general.attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  id:
    description:
      - A O(id) of the image you would like to manage.
    type: int
  name:
    description:
      - A O(name) of the image you would like to manage.
      - Required if O(create=true).
    type: str
  state:
    description:
      - V(present) - state that is used to manage the image.
      - V(absent) - delete the image.
      - V(cloned) - clone the image.
      - V(renamed) - rename the image to the O(new_name).
    choices: ["present", "absent", "cloned", "renamed"]
    default: present
    type: str
  enabled:
    description:
      - Whether the image should be enabled or disabled.
    type: bool
  new_name:
    description:
      - A name that is assigned to the existing or new image.
      - In the case of cloning, by default O(new_name) is set to the name of the origin image with the prefix 'Copy of'.
    type: str
  persistent:
    description:
      - Whether the image should be persistent or non-persistent.
    type: bool
    version_added: 9.5.0
  create:
    description:
      - Whether the image should be created if not present.
      - This is ignored if O(state=absent).
    type: bool
    version_added: 10.0.0
  template:
    description:
      - Use with O(create=true) to specify image template.
    type: str
    version_added: 10.0.0
  datastore_id:
    description:
      - Use with O(create=true) to specify datastore for image.
    type: int
    version_added: 10.0.0
  wait_timeout:
    description:
      - Seconds to wait until image is ready, deleted or cloned.
    type: int
    default: 60
    version_added: 10.0.0
author:
  - "Milan Ilic (@ilicmilan)"
"""

EXAMPLES = r"""
- name: Fetch the IMAGE by id
  community.general.one_image:
    id: 45
  register: result

- name: Print the IMAGE properties
  ansible.builtin.debug:
    var: result

- name: Rename existing IMAGE
  community.general.one_image:
    id: 34
    state: renamed
    new_name: bar-image

- name: Disable the IMAGE by id
  community.general.one_image:
    id: 37
    enabled: false

- name: Make the IMAGE persistent
  community.general.one_image:
    id: 37
    persistent: true

- name: Enable the IMAGE by name
  community.general.one_image:
    name: bar-image
    enabled: true

- name: Clone the IMAGE by name
  community.general.one_image:
    name: bar-image
    state: cloned
    new_name: bar-image-clone
  register: result

- name: Delete the IMAGE by id
  community.general.one_image:
    id: '{{ result.id }}'
    state: absent

- name: Make sure IMAGE is present
  community.general.one_image:
    name: myyy-image
    state: present
    create: true
    datastore_id: 100
    template: |
      PATH = "/var/tmp/image"
      TYPE = "OS"
      SIZE = 20512
      FORMAT = "qcow2"
      PERSISTENT = "Yes"
      DEV_PREFIX = "vd"

- name: Make sure IMAGE is present with a longer timeout
  community.general.one_image:
    name: big-image
    state: present
    create: true
    datastore_id: 100
    wait_timeout: 900
    template: |-
      PATH = "https://192.0.2.200/repo/tipa_image.raw"
      TYPE = "OS"
      SIZE = 82048
      FORMAT = "raw"
      PERSISTENT = "Yes"
      DEV_PREFIX = "vd"
"""

RETURN = r"""
id:
  description: Image ID.
  type: int
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample: 153
name:
  description: Image name.
  type: str
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample: app1
group_id:
  description: Image's group ID.
  type: int
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample: 1
group_name:
  description: Image's group name.
  type: str
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample: one-users
owner_id:
  description: Image's owner ID.
  type: int
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample: 143
owner_name:
  description: Image's owner name.
  type: str
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample: ansible-test
state:
  description: State of image instance.
  type: str
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample: READY
used:
  description: Is image in use.
  type: bool
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample: true
running_vms:
  description: Count of running vms that use this image.
  type: int
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample: 7
permissions:
  description: The image's permissions.
  type: dict
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
  contains:
    owner_u:
      description: The image's owner USAGE permissions.
      type: str
      sample: 1
    owner_m:
      description: The image's owner MANAGE permissions.
      type: str
      sample: 0
    owner_a:
      description: The image's owner ADMIN permissions.
      type: str
      sample: 0
    group_u:
      description: The image's group USAGE permissions.
      type: str
      sample: 0
    group_m:
      description: The image's group MANAGE permissions.
      type: str
      sample: 0
    group_a:
      description: The image's group ADMIN permissions.
      type: str
      sample: 0
    other_u:
      description: The image's other users USAGE permissions.
      type: str
      sample: 0
    other_m:
      description: The image's other users MANAGE permissions.
      type: str
      sample: 0
    other_a:
      description: The image's other users ADMIN permissions.
      type: str
      sample: 0
  sample:
    owner_u: 1
    owner_m: 0
    owner_a: 0
    group_u: 0
    group_m: 0
    group_a: 0
    other_u: 0
    other_m: 0
    other_a: 0
type:
  description: The image's type.
  type: str
  sample: 0
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
disk_type:
  description: The image's format type.
  type: str
  sample: 0
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
persistent:
  description: The image's persistence status (1 means true, 0 means false).
  type: int
  sample: 1
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
source:
  description: The image's source.
  type: str
  sample: /var/lib/one//datastores/100/somerandomstringxd
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
path:
  description: The image's filesystem path.
  type: str
  sample: /var/tmp/hello.qcow2
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
fstype:
  description: The image's filesystem type.
  type: str
  sample: ext4
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
size:
  description: The image's size in MegaBytes.
  type: int
  sample: 10000
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
cloning_ops:
  description: The image's cloning operations per second.
  type: int
  sample: 0
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
cloning_id:
  description: The image's cloning ID.
  type: int
  sample: -1
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
target_snapshot:
  description: The image's target snapshot.
  type: int
  sample: 1
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
datastore_id:
  description: The image's datastore ID.
  type: int
  sample: 100
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
datastore:
  description: The image's datastore name.
  type: int
  sample: image_datastore
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
vms:
  description: The image's list of VM ID's.
  type: list
  elements: int
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample:
    - 1
    - 2
    - 3
  version_added: 9.5.0
clones:
  description: The image's list of clones ID's.
  type: list
  elements: int
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample:
    - 1
    - 2
    - 3
  version_added: 9.5.0
app_clones:
  description: The image's list of app_clones ID's.
  type: list
  elements: int
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  sample:
    - 1
    - 2
    - 3
  version_added: 9.5.0
snapshots:
  description: The image's list of snapshots.
  type: list
  returned: when O(state=present), O(state=cloned), or O(state=renamed)
  version_added: 9.5.0
  sample:
    - date: 123123
      parent: 1
      size: 10228
      allow_orphans: 1
      children: 0
      active: 1
      name: SampleName
"""


from ansible_collections.community.general.plugins.module_utils.opennebula import OpenNebulaModule


IMAGE_STATES = [
    "INIT",
    "READY",
    "USED",
    "DISABLED",
    "LOCKED",
    "ERROR",
    "CLONE",
    "DELETE",
    "USED_PERS",
    "LOCKED_USED",
    "LOCKED_USED_PERS",
]


class ImageModule(OpenNebulaModule):
    def __init__(self):
        argument_spec = dict(
            id=dict(type="int"),
            name=dict(type="str"),
            state=dict(type="str", choices=["present", "absent", "cloned", "renamed"], default="present"),
            enabled=dict(type="bool"),
            new_name=dict(type="str"),
            persistent=dict(type="bool"),
            create=dict(type="bool"),
            template=dict(type="str"),
            datastore_id=dict(type="int"),
            wait_timeout=dict(type="int", default=60),
        )
        required_if = [
            ["state", "renamed", ["id"]],
            ["create", True, ["template", "datastore_id", "name"]],
        ]
        mutually_exclusive = [
            ["id", "name"],
        ]

        OpenNebulaModule.__init__(
            self,
            argument_spec,
            supports_check_mode=True,
            mutually_exclusive=mutually_exclusive,
            required_if=required_if,
        )

    def run(self, one, module, result):
        params = module.params
        id = params.get("id")
        name = params.get("name")
        desired_state = params.get("state")
        enabled = params.get("enabled")
        new_name = params.get("new_name")
        persistent = params.get("persistent")
        create = params.get("create")
        template = params.get("template")
        datastore_id = params.get("datastore_id")
        wait_timeout = params.get("wait_timeout")

        self.result = {}

        image = self.get_image_instance(id, name)
        if not image and desired_state != "absent":
            if create:
                self.result = self.create_image(name, template, datastore_id, wait_timeout)
            # Using 'if id:' doesn't work properly when id=0
            elif id is not None:
                module.fail_json(msg=f"There is no image with id={id}")
            elif name is not None:
                module.fail_json(msg=f"There is no image with name={name}")

        if desired_state == "absent":
            self.result = self.delete_image(image, wait_timeout)
        else:
            if persistent is not None:
                self.result = self.change_persistence(image, persistent)
            if enabled is not None:
                self.result = self.enable_image(image, enabled)
            if desired_state == "cloned":
                self.result = self.clone_image(image, new_name, wait_timeout)
            elif desired_state == "renamed":
                self.result = self.rename_image(image, new_name)

        self.exit()

    def get_image(self, predicate):
        # Filter -2 means fetch all images user can Use
        pool = self.one.imagepool.info(-2, -1, -1, -1)

        for image in pool.IMAGE:
            if predicate(image):
                return image

        return None

    def get_image_by_name(self, image_name):
        return self.get_image(lambda image: (image_name == image.NAME))

    def get_image_by_id(self, image_id):
        return self.get_image(lambda image: (image_id == image.ID))

    def get_image_instance(self, requested_id, requested_name):
        # Using 'if requested_id:' doesn't work properly when requested_id=0
        if requested_id is not None:
            return self.get_image_by_id(requested_id)
        else:
            return self.get_image_by_name(requested_name)

    def create_image(self, image_name, template, datastore_id, wait_timeout):
        if not self.module.check_mode:
            image_id = self.one.image.allocate(f'NAME = "{image_name}"\n{template}', datastore_id)
            self.wait_for_ready(image_id, wait_timeout)
            image = self.get_image_by_id(image_id)
            result = self.get_image_info(image)

        result["changed"] = True
        return result

    def wait_for_ready(self, image_id, wait_timeout=60):
        import time

        start_time = time.time()

        while (time.time() - start_time) < wait_timeout:
            image = self.one.image.info(image_id)
            state = image.STATE

            if state in [IMAGE_STATES.index("ERROR")]:
                self.module.fail_json(msg=f"Got an ERROR state: {image.TEMPLATE['ERROR']}")

            if state in [IMAGE_STATES.index("READY")]:
                return True

            time.sleep(1)
        self.module.fail_json(msg="Wait timeout has expired!")

    def wait_for_delete(self, image_id, wait_timeout=60):
        import time

        start_time = time.time()

        while (time.time() - start_time) < wait_timeout:
            # It might be already deleted by the time this function is called
            try:
                image = self.one.image.info(image_id)
            except Exception:
                check_image = self.get_image_instance(image_id)
                if not check_image:
                    return True

            state = image.STATE

            if state in [IMAGE_STATES.index("DELETE")]:
                return True

            time.sleep(1)

        self.module.fail_json(msg="Wait timeout has expired!")

    def enable_image(self, image, enable):
        image = self.one.image.info(image.ID)
        changed = False

        state = image.STATE

        if state not in [IMAGE_STATES.index("READY"), IMAGE_STATES.index("DISABLED"), IMAGE_STATES.index("ERROR")]:
            if enable:
                self.module.fail_json(msg=f"Cannot enable {IMAGE_STATES[state]} image!")
            else:
                self.module.fail_json(msg=f"Cannot disable {IMAGE_STATES[state]} image!")

        if (enable and state != IMAGE_STATES.index("READY")) or (
            not enable and state != IMAGE_STATES.index("DISABLED")
        ):
            changed = True

        if changed and not self.module.check_mode:
            self.one.image.enable(image.ID, enable)

        result = self.get_image_info(image)
        result["changed"] = changed

        return result

    def change_persistence(self, image, enable):
        image = self.one.image.info(image.ID)
        changed = False

        state = image.STATE

        if state not in [IMAGE_STATES.index("READY"), IMAGE_STATES.index("DISABLED"), IMAGE_STATES.index("ERROR")]:
            if enable:
                self.module.fail_json(msg=f"Cannot enable persistence for {IMAGE_STATES[state]} image!")
            else:
                self.module.fail_json(msg=f"Cannot disable persistence for {IMAGE_STATES[state]} image!")

        if (enable and state != IMAGE_STATES.index("READY")) or (
            not enable and state != IMAGE_STATES.index("DISABLED")
        ):
            changed = True

        if changed and not self.module.check_mode:
            self.one.image.persistent(image.ID, enable)

        result = self.get_image_info(image)
        result["changed"] = changed

        return result

    def clone_image(self, image, new_name, wait_timeout):
        if new_name is None:
            new_name = f"Copy of {image.NAME}"

        tmp_image = self.get_image_by_name(new_name)
        if tmp_image:
            result = self.get_image_info(image)
            result["changed"] = False
            return result

        if IMAGE_STATES.index("DISABLED") == image.STATE:
            self.module.fail_json(msg="Cannot clone DISABLED image")

        if not self.module.check_mode:
            new_id = self.one.image.clone(image.ID, new_name)
            self.wait_for_ready(new_id, wait_timeout)
            image = self.one.image.info(new_id)

        result = self.get_image_info(image)
        result["changed"] = True

        return result

    def rename_image(self, image, new_name):
        if new_name is None:
            self.module.fail_json(msg="'new_name' option has to be specified when the state is 'renamed'")

        if new_name == image.NAME:
            result = self.get_image_info(image)
            result["changed"] = False
            return result

        tmp_image = self.get_image_by_name(new_name)
        if tmp_image:
            self.module.fail_json(msg=f"Name '{new_name}' is already taken by IMAGE with id={tmp_image.ID!s}")

        if not self.module.check_mode:
            self.one.image.rename(image.ID, new_name)

        result = self.get_image_info(image)
        result["changed"] = True
        return result

    def delete_image(self, image, wait_timeout):
        if not image:
            return {"changed": False}

        if image.RUNNING_VMS > 0:
            self.module.fail_json(msg=f"Cannot delete image. There are {image.RUNNING_VMS!s} VMs using it.")

        if not self.module.check_mode:
            self.one.image.delete(image.ID)
            self.wait_for_delete(image.ID, wait_timeout)

        return {"changed": True}


def main():
    ImageModule().run_module()


if __name__ == "__main__":
    main()
